feat: flet test on-device integration testing + CI matrix#6623
Conversation
`ValueKey(controlKey.value)` produced `ValueKey<Object>(value)` because
`controlKey.value` is statically typed `Object`. Flutter's `ValueKey.==`
is runtimeType-strict, so `ValueKey<Object>('foo')` never equals
`ValueKey<String>('foo')` — making `find.byKey(Key('foo'))` /
`find.byKey(ValueKey('foo'))` in flutter_test fail to locate any
Flet-rendered control by user-assigned key.
Switch-dispatch on the runtime type so a String value yields
`ValueKey<String>`, int → `ValueKey<int>`, etc. This matches what
`Key('foo')` (factory for `ValueKey<String>('foo')`) and analogous
test-side constructions produce.
Repro: flet_example in flet-dev/serious-python on the dart-bridge
branch — its integration_test/app_test.dart with
`find.byKey(Key('increment'))` for an IconButton with
`key="increment"` was finding 0 widgets until this fix.
Adds a third transport (`FletDartBridgeServer` + Dart-side channel-builder
injection) that exchanges Flet's MsgPack protocol over the in-process
`dart_bridge` byte channel — the prebuilt-binary FFI bridge consumed by
serious_python plugins via `package:serious_python/bridge.dart`.
Coexists with the existing UDS / TCP socket transport. Activation:
- Python: `FLET_DART_BRIDGE_PORT=<port>` env var + `is_embedded()` true.
- Dart: pass `FletApp(channelBuilder: ...)` — the embedder constructs a
`FletBackendChannel` impl wrapping a `PythonBridge` and feeds it in.
`flet` package itself stays Python-independent: it does NOT depend on
`serious_python` or know about `PythonBridge`. The whole PythonBridge
wiring lives in the embedder's code (proven by a forthcoming
`flet_ffi_example` in serious-python). What lands here in `flet` is just
the seam.
Python side:
- New `flet/messaging/flet_dart_bridge_server.py` — `FletDartBridgeServer`
with the same protocol dispatch as `FletSocketServer`, lazy-imported so
non-embedded runs never load `dart_bridge`. Inbound: `__on_bytes`
enqueues payloads from the C-callback thread onto an asyncio.Queue
drained by `__inbound_loop`. Outbound: `send_message` calls
`dart_bridge.send_bytes(port, packb(...))`.
- `flet/app.py`: `run_async` selection block grows a third arm:
if is_embedded() and FLET_DART_BRIDGE_PORT: dart_bridge
elif is_socket_server: socket (existing)
else: web (existing)
- New helper `__run_dart_bridge_server` modelled on `__run_socket_server`.
Dart side:
- New `FletBackendChannelBuilder` typedef in
`transport/flet_backend_channel.dart`.
- `FletApp` accepts optional `channelBuilder`; `FletBackend` honours it in
`connect()` and skips the URL-scheme factory when present. URL-based
routing for socket / websocket / mock / Pyodide is unchanged.
Wire protocol — unchanged. Same `[ClientAction, body]` MsgPack frames,
same encoder/decoder, same Session dispatch. Only the byte transport
differs.
…t.dart (lets embedders implement channelBuilder)
The dart_bridge transport has no accept loop equivalent — start() registers a byte handler with libdart_bridge and returns immediately. Without an explicit wait, run_async() falls through to conn.close() as soon as main() returns, killing the bridge before any Dart-side frame can arrive. The embedded interpreter then exits even though the Flutter host is still running. Mirror the existing url_prefix/socket-server arm: wait on the terminate event when is_embedded() and FLET_DART_BRIDGE_PORT are both set.
Switches the production transport in `flet build`'s generated app from
TCP/UDS sockets to the in-process dart_bridge FFI channel that the
serious-python `dart-bridge` branch exposes. Web mode (websocket) and
developer mode (external Python process over TCP/UDS) stay unchanged —
PythonBridge only makes sense when the Python interpreter is embedded
in the same OS process as Flutter.
main.dart:
* Two long-lived PythonBridge instances created in prepareApp():
`_bridge` carries the MsgPack-framed Flet protocol; `_exitBridge`
is a dedicated outbound channel for Python's exit code (replaces
the legacy stdout-callback ServerSocket).
* pageUrl = `dartbridge://<port>`; env exports FLET_DART_BRIDGE_PORT
and FLET_DART_BRIDGE_EXIT_PORT. The Python flet package's app.py
picks up FLET_DART_BRIDGE_PORT and starts FletDartBridgeServer
instead of FletSocketServer.
* `_DartBridgeBackendChannel` (lifted from flet_ffi_example): wraps
PythonBridge as a FletBackendChannel — streaming msgpack decoder
on inbound, encoder + 30s retry loop on outbound. Injected into
FletApp via the `channelBuilder` parameter added in the flet PR.
* runPythonApp drops the ServerSocket setup; subscribes to
`_exitBridge.messages` and reuses the existing error-screen /
`exit(code)` handling unchanged.
* Dropped the now-unused `getUnusedPort` helper.
python.dart:
* Drops the `socket` callback channel and FLET_PYTHON_CALLBACK_SOCKET_ADDR.
* `flet_exit` posts the exit code as raw UTF-8 bytes via
`dart_bridge.send_bytes(FLET_DART_BRIDGE_EXIT_PORT, ...)`.
* stdout/stderr → FLET_APP_CONSOLE file redirection preserved (the
Dart side reads it for the error screen on `flet_exit(100)`).
pubspec.yaml:
* `serious_python` pinned to the dart-bridge branch via git ref —
1.0.1 on pub.dev predates PythonBridge. Swap to a version pin
once serious_python ships a release carrying the bridge API.
* Added `msgpack_dart: ^1.0.1` for the channel's wire codec.
Dev mode (--debug + page URL in args) still creates no bridges and
FletApp resolves transport via its URL-scheme factory; web mode reads
Uri.base unchanged.
Add a `path: src/serious_python` entry to the serious-python git dependency in sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml. This directs the package resolver to the subdirectory within the referenced repo (ref: dart-bridge) so the Dart package is loaded from src/serious_python instead of the repository root.
Brings in the multi-version bundled Python support from PR #6577. Conflict in `sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml` resolved by keeping dart-bridge's git-ref dependency on serious-python's `dart-bridge` branch + `msgpack_dart` (required by the in-process dart_bridge FFI transport). Swap back to `serious_python: ^2.0.0` once the FFI transport lands on a tagged serious-python release.
…ld vars
Mirror the serious-python registry bump:
* 3.12 row: Astral PBS date 20260610 (CPython 3.12.13 unchanged).
* 3.13 row: CPython 3.13.14, PBS date 20260610.
* 3.14 row: CPython 3.14.6, PBS date 20260610, Pyodide 314.0.0 GA.
* All three rows gain `python_build_date: "20260611"` for the new
date-keyed flet-dev/python-build release scheme.
The 3.13 wheel platform tag was also wrong — `pyodide-2025.0-wasm32`
where it should have been `pyemscripten-2025.0-wasm32` (the prefix
transition happened at Pyodide 0.28/0.29, not at 314.0). `flet build web
--python-version 3.13` would have failed to match Pyodide-built native
wheels. Fixed in the registry and called out in the 0.86.0 changelog.
`build_base.py` now exports two new env vars alongside the existing
`SERIOUS_PYTHON_VERSION` so the serious-python platform plugin build
scripts can construct the new URL form (`…/<YYYYMMDD>/python-*-<full>-*`):
* SERIOUS_PYTHON_FULL_VERSION → python_release.standalone
* SERIOUS_PYTHON_BUILD_DATE → python_release.python_build_date
Both are set in `package_env` (for `serious_python:main package`) and
`build_env` (for the subsequent `flutter build`).
Breaking-changes docs for 0.86: new 0.86.0 section in the index plus two
new guide pages covering (a) the default-Python bump 3.12 → 3.14 with
three pin options, and (b) the removal of `flet.version.pyodide_version`
/ `PYODIDE_VERSION` with the registry-lookup replacement. The dart_bridge
default-transport migration guide is intentionally not in this commit;
it'll be authored separately.
Publish docs tables (`publish/index.md`, `publish/web/static-website`)
updated to the new patch + Pyodide versions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds dedicated byte channels (`ft.DataChannel`) that let widgets exchange
bulk binary data (image frames, audio buffers, ML tensors) with their
Python counterpart without going through the MsgPack control protocol.
Architecture:
* `package:flet` exposes abstract `DataChannel` + `DataChannelFactory`.
Embedders inject a fast-path factory; absent that, a built-in
`ProtocolMuxedDataChannelFactory` muxes channel bytes over the active
Flet protocol transport.
* Python side: `ft.DataChannel` ABC with `_DartBridgeDataChannel`
(embedded native, dedicated PythonBridge) and `_ProtocolMuxedDataChannel`
(muxed fallback) impls. `Control.get_data_channel(id)` resolves a
channel allocated on the Dart side.
* Handshake: control-level event `data_channel_open` carrying
`{channel_name, channel_id}` — push-driven, no polling, no race.
Wire format change (breaking):
* All transports now prefix every packet with a 1-byte type
discriminator: `0x00` = MsgPack-encoded Flet protocol frame,
`0x01` = raw DataChannel frame (`[channel_id:u32 LE][bytes]`).
* Stream-oriented transports (UDS/TCP) gain a 4-byte little-endian
length prefix; message-oriented transports (WebSocket, postMessage,
dart_bridge) keep native message boundaries.
* `StreamingMsgpackDeserializer` removed — every inbound packet is one
complete MsgPack value, decoded via one-shot `msgpack.deserialize`.
Same simplification on the Python side: `Unpacker.feed` loops →
`msgpack.unpackb(payload)`.
Updated all four Connection subclasses (`FletSocketServer`,
`FletDartBridgeServer`, `flet_web.fastapi.FletApp`, `PyodideConnection`)
and all five Dart transports (socket, WebSocket, JavaScript/postMessage,
mock, JS stub) to the new framing. Pyodide outbound uses Transferable
ArrayBuffer for zero-copy across the worker boundary.
Three smoke tests in `packages/flet/test/transport/data_channel_test.dart`
cover factory allocation, inbound routing by channel id, and the outbound
muxed packet shape.
Replaces the `_invoke_method`-based `apply_full` / `apply_diff` / `clear` plumbing with a dedicated `DataChannel` carrying 1-byte-opcode frames (0x01=full PNG, 0x02=diff PNG, 0x03=clear). PNG bytes no longer pay MsgPack encode/decode — they flow at memory-bandwidth-class speed in embedded native mode and at near-bandwidth speed in dev/web modes (raw- byte frames muxed over the protocol transport). Backpressure follows the WebAgg pattern: Dart sends a 1-byte `[0xFF]` ack back over the same channel after each apply chain resolves; the canvas exposes `set_on_frame_applied(callback)` so `MatplotlibChart` clears `_waiting` only after Dart confirms the frame painted, mirroring mpl.js's `img.onload → waiting=false` flow. Without this gate, interactive drags pile up frames in the Dart-side queue and replay in a burst. The 3D example (`examples/.../matplotlib_chart/three_d/main.py`) adds a status bar showing avg full/diff frame size, total bytes transferred, sliding-window transfer speed, FPS, and per-stage latency (dart-side paint vs mpl-side render+idle) so users can see where time is spent. GPU / CPU strategy code in both State subclasses is unchanged — only the source of frames switched from `_invokeMethod(...)` to the channel listener.
…mpat
`flet build web` was failing to compile with errors like "Type 'Pointer'
not found" because the build template's `main.dart` unconditionally
imported `package:serious_python/bridge.dart` and
`package:serious_python/serious_python.dart`, both of which transitively
pull in `dart:ffi` types via `package:serious_python_platform_interface`.
`dart:ffi` isn't available in the web compile target.
Extract everything that touches `serious_python` into a separate
`native_runtime.dart`:
* `initBridges(envVars) → pageUrl` — creates the protocol + exit
PythonBridge instances and stamps env vars.
* `channelBuilder`, `dataChannelFactory` getters for the embedded
PythonBridge-backed transports.
* `runPython(...)` — wraps `SeriousPython.runProgram` + the exit-bridge
listener.
* `extractAppAssets(...)` — wraps `extractAssetZip`.
* The `_DartBridgeBackendChannel`, `_PythonBridgeDataChannel`, and
`_PythonBridgeDataChannelFactory` impls.
`main.dart` now uses a conditional import:
import 'native_runtime_stub.dart'
if (dart.library.ffi) 'native_runtime.dart' as nrt;
On web, the stub (`native_runtime_stub.dart`) is selected — every
entry point either returns null or throws `UnsupportedError`, and is
guarded behind `kIsWeb` so the stub is never reached at runtime. The
result: `flet build web` compiles cleanly without `dart:ffi` ever
entering the compile graph.
No behavior change on native (mobile/desktop) builds — they pick up the
real `native_runtime.dart` via the conditional and execute the same code
that lived in `main.dart` before.
Pyodide >= 0.29 (and 314.0.0, the Python 3.14 line) throws "Classic web
workers are not supported" inside any worker where `importScripts` is
callable. python-worker.js was spawned as a classic worker, so booting
the Python 3.13 / 3.14 lines surfaced a hard error before any user code
ran.
Switch to module workers across both the flet web client and the
`flet build` template:
* `new Worker(url, { type: "module" })` — module workers don't expose
`importScripts`, so Pyodide's check passes.
* `importScripts(pyodideUrl)` → `const { loadPyodide } = await
import(pyodideUrl)` — the dynamic-import form module workers must
use.
* All `pyodideUrl` defaults flip from `pyodide.js` to `pyodide.mjs` —
the ES-module variant has the named export the dynamic import expects.
URL injection paths:
* `flet publish` / `flet run --web` go through `patch_index.py`, which
now injects `pyodide.mjs` URLs (both CDN and `--no-cdn` branches).
* `flet build web` uses the cookiecutter template's index.html, which
was hardcoded at `/pyodide/pyodide.js` regardless of `--no-cdn`.
Replace with a Jinja conditional that honors `cookiecutter.no_cdn`
and uses the new `cookiecutter.pyodide_version` variable for the
jsdelivr CDN URL. `build_base.py` populates `pyodide_version` from
the resolved `python_release.pyodide`.
Forward-compatible across all three supported Pyodide lines:
0.27.7 (Python 3.12), 0.29.4 (Python 3.13), 314.0.0 (Python 3.14).
Older lines accept module workers too; 0.29+ require them.
* CHANGELOG: new features (DataChannel API), improvements (length-prefix framing + type-byte discriminator, StreamingMsgpackDeserializer removed), breaking changes (wire format on stream transports, mixed flet versions across `flet run` CLI and runtime no longer supported). * New breaking-changes guide `data-channel-protocol-upgrade.md` — migration notes for users with custom backends speaking the Flet protocol, plus a heads-up for anyone subclassing `MatplotlibChartCanvas` (the Dart-side `_invokeMethod` handler no longer fires). * Add the new guide to the 0.86.0 entry in the breaking-changes index.
…ayBuffer The worker→main `postMessage` path was structured-cloning every bulk payload (matplotlib PNG frames, etc.) — measurable cost at ~300 KB per frame. Switch to Transferable: extract the Uint8Array's underlying ArrayBuffer and pass it in the second argument to postMessage. Main thread receives the buffer with ownership transferred, no copy. The matching main→worker (Dart→Python) direction already used Transferable since the DataChannel landing. Both directions are now zero-copy across the worker boundary on Pyodide. This does not move the matplotlib bottleneck — that's WASM-compute-bound on mplot3d — but trims a few ms of structured-clone cost per frame and makes the perf budget closer to what the dart_bridge embedded path delivers natively.
The sync `apply_full` + side-channel `_on_frame_applied` callback was losing matplotlib "draw" events in pyodide mode. Sequence: 1. `_receive_loop` reads frame bytes, calls `apply_full(bytes)` — sync, returns immediately. 2. Loop iterates, reads next event from `_receive_queue`. 3. Next event is a `"draw"` notification matplotlib emitted just after the previous frame (figure dirty again from mouse drag). 4. Gate check: `_waiting=True` (ack hasn't arrived from Dart yet) → **drop the event**. 5. Ack arrives 200+ ms later, `_waiting=False`, but the queue is empty and matplotlib doesn't re-emit "draw" until next mouse event. Result in pyodide: ~1.5 fps observed, vs the 0.85 `_invoke_method` implementation's much higher rate. The 0.85 pattern wasn't faster because it lacked an ack — it had one (the INVOKE_METHOD reply). It was faster because `await self._invoke_method(...)` **blocked the `_receive_loop`** during the round-trip, so matplotlib events queued naturally in `_receive_queue` and were processed in order after the await returned, rather than being eagerly drained against a stale gate. Fix: re-introduce the await pattern at the canvas level. * `MatplotlibChartCanvas.apply_full / apply_diff / clear` are now async. Each enqueues a per-frame `asyncio.Future`, sends the channel packet, and awaits the future. * `_on_dart_message` resolves the head future when `[0xFF]` arrives. * `MatplotlibChart._receive_loop` awaits each `apply_*` call — matplotlib events that arrive during the wait stay queued and are processed after the ack returns. Same behaviour shape as 0.85's `_invoke_method` round-trip, but over the DataChannel transport (no msgpack on the bulk payload). * `set_on_frame_applied(cb)` is preserved as a pure observer callback for instrumentation (e.g. the 3D example's stats panel) — no longer load-bearing for backpressure. The 3D example's `apply_full` / `apply_diff` wrappers updated to `async def` + `await` accordingly.
The multi-version Python PR (#6577) removed flet.version.pyodide_version but the 'Get Pyodide version' step still read it, failing every 'Build Flet Client for Web' run. Resolve the version from the flet_cli.utils.python_versions registry instead (default release's Pyodide), and replace the hand-rolled tarball + wheel downloads with flet_cli.utils.pyodide.ensure_pyodide — the hardcoded micropip-0.8.0/packaging-24.2 filenames would have silently broken on the new Pyodide line (3.14's lock resolves micropip 0.11.1), since curl without -f writes 404 pages into the .whl files. Cherry-picked from 2d8f4a1 on fix-android-arch-filtering. Co-authored-by: ndonkoHenri <robotcoder4@protonmail.com>
…link The 0.86 protocol-framing breaking-change guide linked to a DataChannel API reference page that doesn't exist yet — there's no extending-flet/ folder, and no DataChannel doc has been authored. Docusaurus' broken-link scan failed the docs build on every push. Replace the link with prose pointing at the data_channel.py module docstring; dedicated reference pages can land in a follow-up once the API doc generator covers it.
GitHub Actions emitted Node.js 20 deprecation warnings on every job in run 27457389406. Node 20 will be removed from runners 2026-09-16. Bump the affected actions to their latest Node 24 majors across all workflows: - actions/checkout@v4 → v6 - actions/setup-node@v4 → v6 (v6 limited auto-cache to npm, the website uses yarn via corepack — no caching behavior change) - actions/upload-artifact@v4 / v5.0.0 → v7 - actions/download-artifact@v4 → v8 - astral-sh/setup-uv@v6 → v8.2.0 (v8 dropped the major @v8 tag for supply-chain reasons, full tag required) - dart-lang/setup-dart@<old SHA> → v1.7.2 All six actions' action.yml now declare `runs.using: node24`.
`ValueKey(controlKey.value)` produces `ValueKey<Object>` because
`controlKey.value` is statically typed Object. Flutter's `ValueKey.==`
is runtimeType-strict, so `ValueKey<Object>('foo')` never equals
the `ValueKey<String>('foo')` that ControlWidget assigns to the
rendered widget — making `find_by_key("foo")` from Python tests
find 0 widgets.
Mirrors the fix already applied in control_widget.dart (7367050).
Switch-dispatch on the runtime type so String → ValueKey<String>,
int → ValueKey<int>, etc.
Resolves the cascade of "RangeError: no indices are valid: 0" and
"assert 0 == 1" failures across apps, controls/core, controls/material,
controls/cupertino, and controls/theme integration suites.
#6545 renamed 131 example folders (mostly basic/ → descriptive control name, plus example_1/2/3, nested_themes_1/2 collapsing, and removing the basic/ wrapper where there was only one example) but the matching imports in packages/flet/integration_tests/examples/ were never updated. Test collection failed with ModuleNotFoundError on every affected suite (examples/apps, examples/extensions, and examples/controls/{core,cupertino,material}). Rewrites the 45 test files referencing those modules to the new paths derived from the rename history of commit 1b2e914.
…s from it Add a --json flag to 'flet --version' that emits a machine-readable document (Flet/Flutter versions, supported Python/Pyodide table, Linux build deps). CI workflows and the publish docs now read it via jq instead of importing Flet internals with 'python -c'. Move the canonical Linux apt dependency list from flet.utils.linux_deps (runtime package) to flet_cli.utils.linux_deps (build tooling), where it sits next to python_versions.py and is a same-package import for the CLI.
GitHub is redirecting windows-latest to windows-2025-vs2026 by June 15, 2026. Pin the label explicitly in the Flet Build Test and Build & Publish workflows to silence the redirect notice and make the image deterministic.
Update pubspec.yaml dependency for flutter_secure_storage from fixed 10.0.0 to caret ^10.0.0, allowing compatible minor/patch updates instead of pinning to a single patch version. This lets the package accept backwards-compatible releases without manual changes. Fix #6586
Drop flet's hand-mirrored SUPPORTED_PYTHON_VERSIONS table and load the supported Python/Pyodide/dart_bridge set from python-build's date-keyed manifest.json — the single source of truth shared with serious_python. - python_versions.py: pin one PYTHON_BUILD_RELEASE_DATE; fetch that release's manifest.json (cached immutably under ~/.flet/cache/python-build, offline fallback to cache) and parse it lazily. Module constants become get_supported_python_versions()/get_default_python_version(); resolution logic unchanged. Dev/CI overrides: FLET_PYTHON_BUILD_RELEASE_DATE, FLET_PYTHON_BUILD_MANIFEST. - flet build: pass only SERIOUS_PYTHON_VERSION; serious_python derives the full version, build date, and dart_bridge version from its committed snapshot. Drops the SERIOUS_PYTHON_FULL_VERSION/SERIOUS_PYTHON_BUILD_DATE exports. - flet --version: drop the Python/Pyodide matrix (stays offline); --json keeps flet/flutter/linux_dependencies. - ci.yml: read the default Pyodide version via the manifest-backed resolver instead of jq over `flet --version --json`. - Docs: update the removed-pyodide-version-export guide + CHANGELOG to the new accessors; document the pin in CONTRIBUTING. - Add offline tests driven by FLET_PYTHON_BUILD_MANIFEST.
…ression)
screen_brightness_macos 2.1.3 ("Fix: swift package manager warning") ships a
Package.swift declaring macOS 10.11, below FlutterFramework's 10.15 SPM floor,
so `flutter build macos` fails to resolve with Swift Package Manager enabled.
Pinning the app-facing screen_brightness alone doesn't help — the federated
macOS implementation is separately versioned. Override the impl to the last
good 2.1.2 in both the build template and the client app.
Upstream: aaassseee/screen_brightness#99
Switching --python-version (or requires-python) between builds left the previous version's compiled bytecode in the reused build directory's native bundles (stdlib/site-packages .pyc), crashing the app at runtime with `ImportError: bad magic number`. Record the resolved Python short version in the build dir and, when it changes, wipe the build dir so the native bundles are regenerated for the new interpreter.
Let Flet users write and run integration tests for their apps. Tests live in `tests/` and drive the app running on-device (built monolithic app with embedded Python over dart_bridge): find controls by key/text, tap, enter text, assert state and screenshots. - New `flet test` CLI command (mirrors `flet debug`): provisions a Flutter test host via the build pipeline in test mode, packages the app's Python, then runs pytest. Supports platform positional + `--device-id`, `-k`, `-u` (goldens), `-v`. - pytest plugin shipped with `flet` (zero boilerplate): function-scoped `flet_app` fixture (fresh app per test); also runs via plain `uv run pytest`, with `FLET_TEST_PLATFORM`/`FLET_TEST_DEVICE`/`FLET_TEST_GOLDEN` env overrides. - Independent tester channel: Dart `RemoteWidgetTester` <-> Python `RemoteTester` over a raw socket with length-prefixed JSON frames, separate from Flet's transport. The Flutter WidgetTester driver moved into `packages/flet` behind `package:flet/testing.dart`, shared by host (`runFletHostTest`) and device (`runFletDeviceTest`) modes. - `flet create` scaffolds `tests/test_main.py` + pytest config; build template gains a test_mode-gated integration_test entry point. - Docs: getting-started/integration-testing guide + cli/flet-test reference.
…est package `dart pub publish --dry-run` for `packages/flet` failed: its lib/ imported the dev-only `flutter_test`/`integration_test` packages, which pub forbids (packages used in lib/ must be in `dependencies`). Putting the driver inside `flet` was the wrong call — it can't ship to pub.dev that way. Move the concrete Flutter driver (flutter_tester, flutter_test_finder, device_test, host_test, remote_widget_tester, frame_decoder) into a new `packages/flet_integration_test` package (publish_to: none) that depends on flet + integration_test. flet's published lib/ no longer references any test-only package; the abstract Tester/TestFinder interfaces stay in flet as before. - packages/flet: drop integration_test dev-dep, remove lib/testing.dart entry. - packages/flet_integration_test: new package; cross-package imports of Tester/TestFinder/Lock collapse to package:flet/flet.dart; redundant dart:io imports dropped (flet re-exports it). Standard Flutter .gitignore. - client + build template: import package:flet_integration_test instead of package:flet/testing.dart; add it as a path dev-dependency (test_mode-gated in the template). - build_base: for local dev, rewrite the flet_integration_test path the same way it already rewrites flet (it's publish_to:none, only resolvable from the repo). Verified: flet `pub publish --dry-run` passes; flet_integration_test and client integration_test analyze clean.
The build template referenced flet_integration_test by a repo-relative path
gated with a Jinja `{% if %}` block. That broke two things for the released
(zipped) template:
1. The release pipeline patches the template pubspec with patch_pubspec_version.py,
which does a yaml.safe_load round-trip. The uncommented `{% if %}` block made
the pubspec invalid YAML, so the patch/zip step would fail on tag.
2. A repo-relative path can't resolve once the template is zipped and shipped.
Stop putting the test dependencies in the template pubspec. flet-cli now injects
them after rendering (build_base.create_flutter_project), gated on test_mode:
- local dev: flet_integration_test by path (+ dependency_override), like flet.
- end user: flet_integration_test as a git dependency pinned to this flet
version's tag (the package is publish_to:none, never on pub.dev) — consistent
with how the template already pulls serious_python from git.
The template pubspec is now plain valid YAML again (the patch tooling round-trips
it cleanly) and a normal `flet build` never pulls the test driver.
flet_integration_test depends on flet by version (not path) with a local
dependency_override, so flet unifies to a single source across the graph whether
it's consumed by path (repo) or git (user); flutter_test becomes a regular dep so
test hosts get it transitively.
Verified: template pubspec parses; patch_pubspec_version.py round-trips it in both
release and dev modes; `flet test` provisioning injects the deps and
`flutter pub get` resolves; flet_integration_test analyzes clean.
…t (Windows)
On Windows the device-mode run failed at fixture setup with
`FileNotFoundError [WinError 2]`: FletTestApp spawned `flutter test` via
`create_subprocess_exec("flutter", ...)`, but Windows' CreateProcess does no
PATHEXT lookup, so a bare "flutter" (really `flutter.bat`) isn't found.
`flet test` already resolves the real Flutter executable (full path, `.bat` on
Windows) for provisioning — pass it to the pytest subprocess as
`FLET_TEST_FLUTTER_EXE` (and propagate it via `_TEST_ENV_KEYS`), and have
FletTestApp use it as argv[0], falling back to a bare "flutter" on PATH.
…desktop) Windows and Linux `flet test` failed after a successful build with `Unable to start executable ... Failed to find "<project_name>.exe/binary"`. Root cause (the "doesn't build on desktop" hypothesis was wrong — Flutter does build): the Windows/Linux runner sets the executable OUTPUT_NAME to `artifact_name`, but `flutter test -d <desktop>` launches the binary by the Flutter pubspec `name` (== project_name). When `[tool.flet] artifact` differs from the project name (e.g. `flet-test-counter` vs `flet_test_counter`), the built binary and the launched path don't match. macOS is unaffected (its `.app` is located by the product/artifact name). In test mode, pin `artifact_name = project_name` so the desktop binary's name matches what the integration-test host launches. Verified: macOS still passes (now builds `flet_test_counter.app`); fixes the Windows/Linux launch path.
flet test spawns its own 'flutter test integration_test' (via FletTestApp) instead of going through _run_flutter_command, so it never received the serious_python build-time env that flet build sets. Most critically SERIOUS_PYTHON_APP was unset, which makes the Android packageApp Gradle task early-return and leave a stale app.zip (old-Python main.pyc) in the APK, crashing the embedded runtime with 'ImportError: bad magic number'. Extract the serious_python native-build env into a shared _serious_python_build_env() and use it from both _run_flutter_command and flet test's _flutter_path_env, so the two paths bundle an identical app and can't drift. Adds SERIOUS_PYTHON_APP, SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES and SP_NATIVE_SET to the test env (and _TEST_ENV_KEYS).
Android: stream device-side logs (embedded Python stdout/stderr, Flet, Flutter, native crashes) to a file during the run and dump them in a collapsible group afterwards, so on-device failures are diagnosable from CI. Linux: xvfb has no GPU, so the Flutter GTK app crashes on GL context creation (exit 79); install Mesa's software GL (llvmpipe) and force it via LIBGL_ALWAYS_SOFTWARE/GALLIUM_DRIVER.
Android: stop streaming logcat live (verbose device logs bog down the software emulator and stall the job); instead dump the relevant slice of the ring buffer after the run with non-blocking 'adb logcat -d'. Linux: add a failure-diagnostic step that reports the active GL renderer (glxinfo) and runs the built bundle directly to surface its exit-79 crash output, which the test harness otherwise swallows.
…nostics
The counter test asserted on the first frame, but on a device the embedded
Python cold start (interpreter init + import flet + main()) can take several
seconds — longer than the device driver's fixed warmup — so find_by_text('0')
ran before the app rendered and returned 0 on the slow CI emulator (passed
locally on a faster one). pump_and_settle only settles Flutter frames, not a
pending python->dart round-trip, so poll for the first render instead.
CI: make the android logcat dump run even on failure (|| CODE=$?) with a
tight python+crash filter that won't bog the emulator; cap the linux
bundle-diagnostic with timeout so it can't hang the job.
Android: drop the background 'adb logcat &' (a streaming child can keep the emulator-runner script from finishing); dump the ring buffer after the run with non-blocking 'adb logcat -d' instead. Linux: the app and software GL are fine (glxinfo shows llvmpipe; the bundle runs directly without crashing) — exit 79 is specific to the integration_test path, which enables the semantics tree and makes GTK embed an ATK a11y socket that doesn't exist under xvfb. Disable the AT-SPI bridge (NO_AT_BRIDGE/GTK_A11Y).
… to artifact The android job reported success while pytest actually failed: the emulator-runner ran the multi-line script such that 'exit $CODE' saw an empty CODE (and a '\'-continuation in the logcat line broke, dumping the entire unfiltered logcat = ~58k console lines). Run the script as a single folded line so the test command is last and its exit code is the job's, and write a filtered device log (embedded Python + crashes only) to a file via an EXIT trap, uploaded as an artifact instead of streaming to the console. Also: the counter never rendered within the 10s poll window on the slow CI emulator (cold-start embedded Python is much slower there), so poll on a 60s deadline instead of a fixed 40 attempts.
…anch Temporarily override serious_python_android + serious_python_platform_interface to flet-dev/serious-python#218 (fix/android-x86_64-sysconfigdata) so the android x86_64 CI leg validates the fix end-to-end (embedded Python no longer crashes with ModuleNotFoundError: _sysconfigdata__android_x86_64-linux-android). Locally confirmed: pubspec.lock resolves to the branch and stdlib.zip now ships both aarch64 and x86_64 _sysconfigdata. Revert to the pub.dev release once #218 ships.
The android on-device run can wedge (emulator goes offline) and run until the default 6h limit; cap the job at 40 minutes.
After an on-device test passes, teardown calls RemoteTester.stop(), which did 'await self._server.wait_closed()' with no timeout. wait_closed() blocks until the active _handle_client finishes, but _read_loop blocks on readexactly() until EOF — and the on-device app's socket close doesn't always deliver EOF to us (seen on Linux), so the asyncio loop hung forever after 'All tests passed!' (the flet test process never exits). Cancel the read task so _handle_client completes, close the writer, and bound wait_closed() with a timeout.
The linux job fails with 'No tests were found' + exit 79 on the x86_64 official flutter (passes on arm64). flet_test_app already uses the file-form target and verifies app_test.dart is non-empty, so it's neither. Re-run the integration test directly with --verbose (unreachable dummy server) to capture which build target/entrypoint flutter uses, whether the testWidgets body runs, and the exit reason; upload the full verbose log as an artifact.
The dir->file change in 17d368b was not what fixed Linux (the window-realize / ready-to-show fixes were; both the dir and file forms reported 'No tests were found' until then). Revert the flutter test target to the directory form ('integration_test') and keep only the useful part: in device mode, validate the generated integration_test/app_test.dart exists and is non-empty so a missing/empty driver surfaces as a clear error instead of a confusing 'No tests were found'.
Drop the _find_text_when_ready polling helper. It was a band-aid for the android render race, but the real cause was the serious_python x86_64 crash (PR #218) — now fixed. Try the plain template-style test and let CI confirm the counter renders in time on the slow emulator.
Update .fvmrc to pin Flutter version 3.44.4 (patch bump from 3.44.3) to ensure a consistent SDK across development and CI environments.
- mesa-utils was only used by glxinfo in the (removed) Diagnose Linux step. - NO_AT_BRIDGE/GTK_A11Y were added mid-debugging but didn't fix Linux (the window realize / ready-to-show change did); the Atk-CRITICAL warnings were non-fatal. Remove them and the now-inaccurate comment. Keep the software-GL env (xvfb has no GPU) and the android logcat artifact (only window into on-device failures).
Flutter publishes no prebuilt arm64 Linux SDK (releases are x64-only), so flet-cli's install_flutter downloaded a broken x64 tarball on arm64 Linux. For arm64 Linux, clone the SDK at the version tag instead; the first 'flutter' run then fetches the arch-appropriate engine/Dart artifacts (how fvm/git installs work). Add a 'linux-arm64' CI leg (ubuntu-24.04-arm) that skips the Flutter setup action so 'flet test' installs Flutter via this path, exercising it end-to-end.
A bare git clone has no bin/cache, so 'dart run serious_python:main' failed with 'could not find package sky_engine ... solving failed'. Run 'flutter precache --linux' right after the clone to populate the engine artifacts (sky_engine + the Linux desktop engine) the prebuilt archives ship.
…ts version - CI matrix now crosses each platform with python 3.12/3.13/3.14 (job env PYTHON_VERSION/EXPECTED_PYTHON_VERSION from matrix; dropped the workflow_dispatch python_version input the matrix supersedes). - Counter app displays 'Python <platform.python_version()>'. - test_counter asserts the app reports the expected major.minor (EXPECTED_PYTHON_VERSION), falling back to 'any version shown' locally. Validated on macOS (renders Python 3.14.6; 1 passed).
serious_python 4.1.1 (with the x86_64 _sysconfigdata fix, PR #218) is on pub.dev. Bump the build template serious_python 4.1.0 -> 4.1.1 and remove the temporary git override from flet_test_counter (the fix branch was deleted after release, which broke 'flutter pub get' on fresh runners — the linux-arm64 legs failed with 'could not find git ref fix/android-x86_64-sysconfigdata').
| subprocess.run( | ||
| ["git", "clone", "--depth", "1", "--branch", version] | ||
| + [FLUTTER_GIT_URL, install_dir], | ||
| check=True, | ||
| ) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| ) | ||
| log(f"Precaching Flutter {version} engine artifacts...") | ||
| flutter_exe = os.path.join(install_dir, "bin", "flutter") | ||
| subprocess.run([flutter_exe, "precache", "--linux"], check=True) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
It imported FletTestApp, which pulls in the screenshot-comparison deps (numpy/Pillow/scikit-image) from the optional 'test' extra at module load. The base unit-test suite installs flet without that extra, so collection failed with ModuleNotFoundError: No module named 'numpy'. The __flutter_test_target device-mode guard is exercised end-to-end by the flet-test on-device workflow.
Deploying flet-website-v2 with
|
| Latest commit: |
3dea72a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7a8747be.flet-website-v2.pages.dev |
| Branch Preview URL: | https://flet-test.flet-website-v2.pages.dev |
Summary
Adds
flet test— on-device integration testing that builds a per-platformtest host with embedded Python, launches the app on the real device/emulator/
simulator, and drives it over a socket tester channel (find controls by key,
tap, assert). Also adds a CI workflow that runs it end-to-end across every
target and Python version, plus the fixes needed to make all of them green.
What's included
flet testcommand + theflet_integration_testdriver package; thedevice-mode driver is injected at build time.
flet-test.ymlworkflow — a matrix of{macos, ios, windows, linux, linux-arm64, android} × {Python 3.12, 3.13, 3.14}(18 jobs). Each legprovisions the host, runs the bundled Counter app with embedded Python, and
drives it; the app displays its embedded Python version and the test asserts
the expected major.minor.
flet_test_counterexample app + test (the template others can copy).Notable fixes along the way
ModuleNotFoundError: _sysconfigdata__android_x86_64-linux-android— theABI-common
stdlib.zipdropped non-primary ABIs' arch-specific sysconfigdata.Fixed in serious_python (4.1.1, now used from pub.dev).
flet testnative-build env: pass the same serious_python env (incl.SERIOUS_PYTHON_APP) thatflet builduses, so the on-device app isn't builtfrom a stale
app.zip.rendered under headless software GL — realize the Flutter view early and skip
the
window_managerready-to-show wait under test (Linux only; productionunaffected).
flet-clinowgit-clones the SDK at the version tag and precaches its engine artifacts.
RemoteTester.stop()could hang onwait_closed()when the appsocket close didn't deliver EOF.
green exit-code propagation on Android, and device-log artifacts for
diagnosing on-device failures.
Validation
Full 18-job matrix green (one transient Android emulator flake passed on
re-run). serious_python is consumed from the released 4.1.1 on pub.dev — no
temporary git overrides remain.
Summary by Sourcery
Introduce on-device integration testing support via the new
flet testcommand and supporting infrastructure, including a cross-platform CI workflow and example app, while aligning build/runtime behavior and documentation with the new testing model.New Features:
flet testCLI command and supporting Python/Dart integration-test drivers to run apps on-device with embedded Python and drive them via a remote tester.flet_test_counterexample app and pytest-based tests as a template for on-device integration testing.flet.testing, enablingflet_appfixtures and integration tests to run via pytest orflet test.Bug Fixes:
flet buildandflet testso embedded Python apps bundle the correct runtime and site-packages.Enhancements:
FletTestAppto support a device mode usingRemoteTester, allowing tests to drive on-device apps over a dedicated socket.Build:
CI:
flet-test.ymlGitHub Actions workflow runningflet testacross a matrix of platforms (desktop, mobile, arm64) and Python versions, including Android emulator setup and log collection.Documentation:
flet test, and add reference pages for testing-related types and CLI command.flet testCLI in the docs.Tests:
FletTestAppdevice-mode Flutter test target validation.flet_test_counterexample with pytest configuration and sample integration tests to exercise the new tooling.Chores:
flet testsubcommand in the CLI, extendflet createoutput with a testing hint, and tweak the repo typos configuration for new terminology.